package health_system.health_system.service.impl;

import health_system.health_system.dto.*;
import health_system.health_system.entity.Follow;
import health_system.health_system.entity.User;
import health_system.health_system.exception.FollowAlreadyExistsException;
import health_system.health_system.exception.FollowNotFoundException;
import health_system.health_system.exception.ResourceNotFoundException;
import health_system.health_system.exception.SelfFollowException;
import health_system.health_system.repository.FollowRepository;
import health_system.health_system.repository.UserRepository;
import health_system.health_system.service.FollowService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class FollowServiceImpl implements FollowService {

    private final FollowRepository followRepository;
    private final UserRepository userRepository;
    
    @Override
    @Transactional
    public FollowDTO followUser(FollowRequestDTO followRequest) {
        return followUserById(followRequest.getFollowerId(), followRequest.getFollowedId());
    }
    
    @Override
    @Transactional
    @CacheEvict(value = {"userFollowers", "userFollowing", "userFollowStats"}, allEntries = true)
    public FollowDTO followUserById(Long followerId, Long followedId) {
        log.debug("Creating follow relationship: followerId={}, followedId={}", followerId, followedId);
        
        // Validate users exist
        User follower = userRepository.findById(followerId)
                .orElseThrow(() -> new ResourceNotFoundException("User", "id", followerId));
                
        User followed = userRepository.findById(followedId)
                .orElseThrow(() -> new ResourceNotFoundException("User", "id", followedId));
        
        // Check if user is trying to follow themselves
        if (Objects.equals(followerId, followedId)) {
            throw new SelfFollowException();
        }
        
        // Check if already following
        if (followRepository.isFollowing(followerId, followedId)) {
            throw new FollowAlreadyExistsException(followerId, followedId);
        }
        
        // Create new follow relationship
        Follow follow = new Follow();
        follow.setFollower(follower);
        follow.setFollowed(followed);
        follow.setIsNotificationEnabled(true);
        
        try {
            Follow savedFollow = followRepository.save(follow);
            return mapToDTO(savedFollow);
        } catch (DataIntegrityViolationException e) {
            // Handle race condition if follow relationship was created concurrently
            log.warn("Concurrent follow creation attempted: {}", e.getMessage());
            throw new FollowAlreadyExistsException(followerId, followedId);
        }
    }
    
    @Override
    @Transactional
    @CacheEvict(value = {"userFollowers", "userFollowing", "userFollowStats"}, allEntries = true)
    public void unfollowUser(Long followerId, Long followedId) {
        log.debug("Removing follow relationship: followerId={}, followedId={}", followerId, followedId);
        
        // Check if follow relationship exists
        Follow follow = followRepository.findByFollowerIdAndFollowedId(followerId, followedId)
                .orElseThrow(() -> new FollowNotFoundException(followerId, followedId));
        
        followRepository.deleteByFollowerIdAndFollowedId(followerId, followedId);
    }
    
    @Override
    @Transactional
    public FollowDTO toggleNotifications(Long followerId, Long followedId, Boolean enabled) {
        log.debug("Toggling notifications for follow relationship: followerId={}, followedId={}, enabled={}", 
                followerId, followedId, enabled);
        
        // Find the follow relationship
        Follow follow = followRepository.findByFollowerIdAndFollowedId(followerId, followedId)
                .orElseThrow(() -> new FollowNotFoundException(followerId, followedId));
        
        // Update notification setting
        follow.setIsNotificationEnabled(enabled);
        Follow updatedFollow = followRepository.save(follow);
        
        return mapToDTO(updatedFollow);
    }
    
    @Override
    @Transactional(readOnly = true)
    public boolean isFollowing(Long followerId, Long followedId) {
        return followRepository.isFollowing(followerId, followedId);
    }
    
    @Override
    @Transactional(readOnly = true)
    public FollowDTO getFollowById(Long followId) {
        log.debug("Getting follow by id: {}", followId);
        
        Follow follow = followRepository.findById(followId)
                .orElseThrow(() -> new FollowNotFoundException(followId));
                
        return mapToDTO(follow);
    }
    
    @Override
    @Transactional(readOnly = true)
    public FollowDTO getFollow(Long followerId, Long followedId) {
        log.debug("Getting follow relationship: followerId={}, followedId={}", followerId, followedId);
        
        Follow follow = followRepository.findByFollowerIdAndFollowedId(followerId, followedId)
                .orElseThrow(() -> new FollowNotFoundException(followerId, followedId));
                
        return mapToDTO(follow);
    }
    
    @Override
    @Transactional(readOnly = true)
    @Cacheable(value = "userFollowers", key = "#userId + '-' + #pageable.pageNumber + '-' + #pageable.pageSize")
    public PageResponseDTO<UserSummaryDTO> getFollowers(Long userId, Pageable pageable) {
        log.debug("Getting followers for user {}, page {}", userId, pageable.getPageNumber());
        
        // Validate user exists
        userRepository.findById(userId)
                .orElseThrow(() -> new ResourceNotFoundException("User", "id", userId));
        
        // Get user from repository
        User user = userRepository.getReferenceById(userId);
        
        // Get followers with pagination
        Page<Follow> followersPage = followRepository.findByFollowed(user, pageable);
        
        // Map to DTOs
        Page<UserSummaryDTO> userDtoPage = followersPage.map(follow -> UserSummaryDTO.fromUser(follow.getFollower()));
        
        return PageResponseDTO.fromPage(userDtoPage);
    }
    
    @Override
    @Transactional(readOnly = true)
    @Cacheable(value = "userFollowing", key = "#userId + '-' + #pageable.pageNumber + '-' + #pageable.pageSize")
    public PageResponseDTO<UserSummaryDTO> getFollowing(Long userId, Pageable pageable) {
        log.debug("Getting following for user {}, page {}", userId, pageable.getPageNumber());
        
        // Validate user exists
        userRepository.findById(userId)
                .orElseThrow(() -> new ResourceNotFoundException("User", "id", userId));
        
        // Get user from repository
        User user = userRepository.getReferenceById(userId);
        
        // Get following with pagination
        Page<Follow> followingPage = followRepository.findByFollower(user, pageable);
        
        // Map to DTOs
        Page<UserSummaryDTO> userDtoPage = followingPage.map(follow -> UserSummaryDTO.fromUser(follow.getFollowed()));
        
        return PageResponseDTO.fromPage(userDtoPage);
    }
    
    @Override
    @Transactional(readOnly = true)
    public List<UserSummaryDTO> getMutualFollows(Long userId) {
        log.debug("Getting mutual follows for user {}", userId);
        
        // Validate user exists
        User user = userRepository.findById(userId)
                .orElseThrow(() -> new ResourceNotFoundException("User", "id", userId));
        
        // Get mutual follows
        List<User> mutualFollows = followRepository.findMutualFollows(user);
        
        // Map to DTOs
        return mutualFollows.stream()
                .map(UserSummaryDTO::fromUser)
                .collect(Collectors.toList());
    }
    
    @Override
    @Transactional(readOnly = true)
    @Cacheable(value = "userFollowStats", key = "#userId + '-' + #currentUserId")
    public FollowStatsDTO getFollowStats(Long userId, Long currentUserId) {
        log.debug("Getting follow stats for user {}", userId);
        
        // Validate user exists
        userRepository.findById(userId)
                .orElseThrow(() -> new ResourceNotFoundException("User", "id", userId));
        
        // Get counts
        Long followersCount = followRepository.countFollowersByUserId(userId);
        Long followingCount = followRepository.countFollowingByUserId(userId);
        
        // Check if current user is following the user
        boolean isFollowing = false;
        if (currentUserId != null && !Objects.equals(currentUserId, userId)) {
            isFollowing = followRepository.isFollowing(currentUserId, userId);
        }
        
        return FollowStatsDTO.builder()
                .userId(userId)
                .followersCount(followersCount)
                .followingCount(followingCount)
                .isCurrentUserFollowing(isFollowing)
                .build();
    }
    
    @Override
    @Transactional(readOnly = true)
    public List<UserSummaryDTO> getSuggestedUsersToFollow(Long userId, int limit) {
        log.debug("Getting suggested users to follow for user {}, limit {}", userId, limit);
        
        // Validate user exists
        User user = userRepository.findById(userId)
                .orElseThrow(() -> new ResourceNotFoundException("User", "id", userId));
        
        // Get suggested users
        List<User> suggestedUsers = followRepository.findSuggestedUsersToFollow(user);
        
        // Apply limit and map to DTOs
        return suggestedUsers.stream()
                .limit(limit)
                .map(UserSummaryDTO::fromUser)
                .collect(Collectors.toList());
    }
    
    @Override
    @Transactional(readOnly = true)
    public List<UserSummaryDTO> getPopularUsers(int limit) {
        log.debug("Getting popular users, limit {}", limit);
        
        // Get popular users with their follower counts
        List<Object[]> popularUsersWithCounts = followRepository.findPopularUsers(PageRequest.of(0, limit));
        
        // Extract users and map to DTOs
        return popularUsersWithCounts.stream()
                .map(array -> UserSummaryDTO.fromUser((User) array[0]))
                .collect(Collectors.toList());
    }
    
    @Override
    @Transactional(readOnly = true)
    public List<UserSummaryDTO> searchFollowers(Long userId, String searchTerm) {
        log.debug("Searching followers for user {} with term: {}", userId, searchTerm);
        
        // Validate user exists
        User user = userRepository.findById(userId)
                .orElseThrow(() -> new ResourceNotFoundException("User", "id", userId));
        
        // Search followers
        List<User> matchingFollowers = followRepository.searchFollowers(user, searchTerm);
        
        // Map to DTOs
        return matchingFollowers.stream()
                .map(UserSummaryDTO::fromUser)
                .collect(Collectors.toList());
    }
    
    @Override
    @Transactional(readOnly = true)
    public List<UserSummaryDTO> searchFollowing(Long userId, String searchTerm) {
        log.debug("Searching following for user {} with term: {}", userId, searchTerm);
        
        // Validate user exists
        User user = userRepository.findById(userId)
                .orElseThrow(() -> new ResourceNotFoundException("User", "id", userId));
        
        // Search following
        List<User> matchingFollowing = followRepository.searchFollowing(user, searchTerm);
        
        // Map to DTOs
        return matchingFollowing.stream()
                .map(UserSummaryDTO::fromUser)
                .collect(Collectors.toList());
    }
    
    @Override
    @Transactional(readOnly = true)
    public List<UserSummaryDTO> getFollowersWhoAreFriends(Long userId) {
        log.debug("Getting followers who are also friends for user {}", userId);
        
        // Validate user exists
        User user = userRepository.findById(userId)
                .orElseThrow(() -> new ResourceNotFoundException("User", "id", userId));
        
        // Get followers who are also friends
        List<User> followersWhoAreFriends = followRepository.findFollowersWhoAreFriends(user);
        
        // Map to DTOs
        return followersWhoAreFriends.stream()
                .map(UserSummaryDTO::fromUser)
                .collect(Collectors.toList());
    }
    
    @Override
    @Transactional(readOnly = true)
    public List<UserSummaryDTO> getFollowingsWhoAreFriends(Long userId) {
        log.debug("Getting followings who are also friends for user {}", userId);
        
        // Validate user exists
        User user = userRepository.findById(userId)
                .orElseThrow(() -> new ResourceNotFoundException("User", "id", userId));
        
        // Get followings who are also friends
        List<User> followingsWhoAreFriends = followRepository.findFollowingsWhoAreFriends(user);
        
        // Map to DTOs
        return followingsWhoAreFriends.stream()
                .map(UserSummaryDTO::fromUser)
                .collect(Collectors.toList());
    }
    
    /**
     * Maps a Follow entity to a FollowDTO
     */
    private FollowDTO mapToDTO(Follow follow) {
        if (follow == null) return null;
        
        return FollowDTO.builder()
                .id(follow.getId())
                .follower(UserSummaryDTO.fromUser(follow.getFollower()))
                .followed(UserSummaryDTO.fromUser(follow.getFollowed()))
                .isNotificationEnabled(follow.getIsNotificationEnabled())
                .createdAt(follow.getCreatedAt())
                .updatedAt(follow.getUpdatedAt())
                .build();
    }
} 